Содержание

  • 1  Введение
  • 2  Обзор данных.
    • 2.1  Вывод по обзору данных.
  • 3  Предобработка данных.
    • 3.1  Смена типа данных.
    • 3.2  Обработка пропусков.
    • 3.3  Дубликаты.
    • 3.4  Проверим столбцы с датами.
    • 3.5  Проверим группировку данных таблиц.
    • 3.6  Вывод по предобработке данных.
  • 4  Оценка корректности проведения теста.
    • 4.1  Проверка соответствия данных требованиям технического задания.
    • 4.2  Время проведения теста. Пересечение с другими активностями.
    • 4.3  Аудитория теста.
      • 4.3.1  Проверка на пересечения с конкурирующим тестом.
      • 4.3.2  Проверка на равномерность распределения по тестовым группам.
    • 4.4  Вывод по корректности проведения теста.
  • 5  Исследовательский анализ данных.
    • 5.1  Количество событий на пользователя одинаково распределены в выборках?
    • 5.2  Как число событий в выборках распределено по дням?
    • 5.3  Как меняется конверсия в воронке в выборках на разных этапах?
    • 5.4  Вывод по исследовательскому анализу.
  • 6  Оценка результатов А/В тестирования.
    • 6.1  Проверка гипотезы о равенстве долей.
  • 7  Общий вывод.

Оценка результатов A/B-теста¶

Введение¶

В вашем распоряжении есть:

  1. Датасет final_ab_events.csv — c действиями новых пользователей в период с 7 декабря 2020 по 4 января 2021 года.

  2. Техническое задание:

  • Название теста: recommender_system_test;
  • группы: А — контрольная, B — новая платёжная воронка;
  • дата запуска: 2020-12-07;
  • дата остановки набора новых пользователей: 2020-12-21;
  • дата остановки: 2021-01-04;
  • аудитория: в тест должно быть отобрано 15% новых пользователей из региона EU;
  • назначение теста: тестирование изменений, связанных с внедрением улучшенной рекомендательной системы;
  • ожидаемое количество участников теста: 6000.
  • ожидаемый эффект: за 14 дней с момента регистрации пользователи покажут улучшение каждой метрики не менее, чем на 10%:
    • конверсии в просмотр карточек товаров — событие product_page,
    • просмотры корзины — product_cart,
    • покупки — purchase.
  1. Несколько вспомогательных датасетов:
  • ab_project_marketing_events.csv — календарь маркетинговых событий на 2020 год

  • final_ab_new_users.csv — пользователи, зарегистрировавшиеся с 7 по 21 декабря 2020 года

  • final_ab_participants.csv — таблица участников тестов

Цели исследования :

  • Оценить корректность проведения теста

  • Проанализировать результаты теста

Ход исследования

  • Провести обзор и предобработку данных

  • Оценить корректность проведения теста. Проверить:

    • Соответствие данных требованиям технического задания

    • Время проведения теста

    • Аудиторию теста

  • Провести исследовательский анализ данных

  • Проанализировать результаты теста

Обзор данных.¶

In [1]:
import pandas as pd  # Импортируем необходимые библиотеки, прочитаем файл и сохраним его в переменную data.

import seaborn as sns
from matplotlib import pyplot as plt
from plotly import graph_objects as go
plt.style.use('ggplot')

import scipy.stats as st
from statsmodels. stats.proportion import proportions_ztest
import numpy as np
import math as mth
import datetime as dt

from pandas.plotting import register_matplotlib_converters
import warnings
register_matplotlib_converters()   
In [2]:
campaings, new_users, events, participants  = (
        pd.read_csv('ab_project_marketing_events.csv'),
        pd.read_csv('final_ab_new_users.csv'),
        pd.read_csv('final_ab_events.csv'),
        pd.read_csv('final_ab_participants.csv')
    )
In [3]:
#функция для обзора данных
def overview_df(df):
    '''Первый взгляд на данные'''
    print('...........Первые 5 строк...........')
    display(df.head())
    print('')
    print('')
    print('...........Тип данных...........')
    print('')
    print(df.info())
In [4]:
overview_df(campaings)
...........Первые 5 строк...........
name regions start_dt finish_dt
0 Christmas&New Year Promo EU, N.America 2020-12-25 2021-01-03
1 St. Valentine's Day Giveaway EU, CIS, APAC, N.America 2020-02-14 2020-02-16
2 St. Patric's Day Promo EU, N.America 2020-03-17 2020-03-19
3 Easter Promo EU, CIS, APAC, N.America 2020-04-12 2020-04-19
4 4th of July Promo N.America 2020-07-04 2020-07-11

...........Тип данных...........

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 14 entries, 0 to 13
Data columns (total 4 columns):
 #   Column     Non-Null Count  Dtype 
---  ------     --------------  ----- 
 0   name       14 non-null     object
 1   regions    14 non-null     object
 2   start_dt   14 non-null     object
 3   finish_dt  14 non-null     object
dtypes: object(4)
memory usage: 576.0+ bytes
None

Датасет campaings содержит календарь маркетинговых событий на 2020 год.

Структура файла:

name — название маркетингового события

regions — регионы, в которых будет проводиться рекламная кампания

start_dt — дата начала кампании

finish_dt — дата завершения кампании

Таблица содержит 14 строк и 4 столбца. Тип данных столбцов с датами необходимо изменить на datetime.

In [5]:
overview_df(new_users)
...........Первые 5 строк...........
user_id first_date region device
0 D72A72121175D8BE 2020-12-07 EU PC
1 F1C668619DFE6E65 2020-12-07 N.America Android
2 2E1BF1D4C37EA01F 2020-12-07 EU PC
3 50734A22C0C63768 2020-12-07 EU iPhone
4 E1BDDCE0DAFA2679 2020-12-07 N.America iPhone

...........Тип данных...........

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 61733 entries, 0 to 61732
Data columns (total 4 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   user_id     61733 non-null  object
 1   first_date  61733 non-null  object
 2   region      61733 non-null  object
 3   device      61733 non-null  object
dtypes: object(4)
memory usage: 1.9+ MB
None

Датасет new_users - все пользователи, зарегистрировавшиеся в интернет-магазине в период с 7 по 21 декабря 2020 года.

Структура файла:

user_id — идентификатор пользователя

first_date — дата регистрации

region — регион пользователя

device — устройство, с которого происходила регистрация

Таблица содержит 61733 строки и 4 столбца. Тип данных столбца с датами необходимо изменить на datetime.

In [6]:
overview_df(events)
...........Первые 5 строк...........
user_id event_dt event_name details
0 E1BDDCE0DAFA2679 2020-12-07 20:22:03 purchase 99.99
1 7B6452F081F49504 2020-12-07 09:22:53 purchase 9.99
2 9CD9F34546DF254C 2020-12-07 12:59:29 purchase 4.99
3 96F27A054B191457 2020-12-07 04:02:40 purchase 4.99
4 1FD7660FDF94CA1F 2020-12-07 10:15:09 purchase 4.99

...........Тип данных...........

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 440317 entries, 0 to 440316
Data columns (total 4 columns):
 #   Column      Non-Null Count   Dtype  
---  ------      --------------   -----  
 0   user_id     440317 non-null  object 
 1   event_dt    440317 non-null  object 
 2   event_name  440317 non-null  object 
 3   details     62740 non-null   float64
dtypes: float64(1), object(3)
memory usage: 13.4+ MB
None

Датасет events — все события новых пользователей в период с 7 декабря 2020 по 4 января 2021 года.

Структура файла:

user_id — идентификатор пользователя

event_dt — дата и время события

event_name — тип события

details — дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах

Таблица содержит 440317 строки и 4 столбца. Наблюдаются пропуски в столбце details. Тип данных столбца с датами необходимо изменить на datetime.

In [7]:
overview_df(participants)
...........Первые 5 строк...........
user_id group ab_test
0 D1ABA3E2887B6A73 A recommender_system_test
1 A7A3664BD6242119 A recommender_system_test
2 DABC14FDDFADD29E A recommender_system_test
3 04988C5DF189632E A recommender_system_test
4 482F14783456D21B B recommender_system_test

...........Тип данных...........

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 18268 entries, 0 to 18267
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   user_id  18268 non-null  object
 1   group    18268 non-null  object
 2   ab_test  18268 non-null  object
dtypes: object(3)
memory usage: 428.3+ KB
None

Датасет participants — таблица участников тестов.

Структура файла:

user_id — идентификатор пользователя

ab_test — название теста

group — группа пользователя

Таблица содержит 18268 строк и 3 столбца.

Вывод по обзору данных.¶

Столбцы с датами необходимо перевести в тип datetime для удобства работы с данными. Изучить пропуски в одном из столбцов.

Предобработка данных.¶

Смена типа данных.¶

Поменяем тип данных столбцов с временем на datetime.

In [8]:
campaings['start_dt'] = pd.to_datetime(campaings['start_dt'])
campaings['finish_dt'] = pd.to_datetime(campaings['finish_dt'])
In [9]:
new_users['first_date'] = pd.to_datetime(new_users['first_date'])
In [10]:
events['event_dt'] = pd.to_datetime(events['event_dt'])

Обработка пропусков.¶

In [11]:
events.isna().agg(['mean', 'sum']).T.sort_values(by='mean', ascending=False) # посмотрим доли пропусков
Out[11]:
mean sum
details 0.857512 377577.0
user_id 0.000000 0.0
event_dt 0.000000 0.0
event_name 0.000000 0.0

В столбце details датафрейма events почти 86 % пропусков. В документации сказано, что в этом столбце хранятся дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах. Это значит, что значения в details зависят от столбца с названиями событий. Посмотрим каким событиям соответствуют пропуски.

In [12]:
events.query("details.isna()").value_counts(['event_name']) # количесвто соответствующих событий с пропусками в details
Out[12]:
event_name  
login           189552
product_page    125563
product_cart     62462
dtype: int64
In [13]:
events.value_counts(['event_name']) # количество событий в общем
Out[13]:
event_name  
login           189552
product_page    125563
purchase         62740
product_cart     62462
dtype: int64

Можно сделать вывод, что в столбце details заполнены значения только по стоимости покупки. Соответственно дополнительных данных для событий-авторизация, карточка товара или продуктовая страница не предусмотрено. Соотвественно оставим пропуски.

Дубликаты.¶

Проверим датафреймы на яные дубликаты.

In [14]:
campaings.duplicated().sum()
Out[14]:
0
In [15]:
new_users.duplicated().sum()
Out[15]:
0
In [16]:
events.duplicated().sum()
Out[16]:
0
In [17]:
participants.duplicated().sum()
Out[17]:
0

Явных дубликатов не обнаружено.

Посмотрим уникальные значения столбцов и проверим их на неявные дубликаты.

In [18]:
campaings['name'].value_counts()
Out[18]:
Christmas&New Year Promo            1
St. Valentine's Day Giveaway        1
St. Patric's Day Promo              1
Easter Promo                        1
4th of July Promo                   1
Black Friday Ads Campaign           1
Chinese New Year Promo              1
Labor day (May 1st) Ads Campaign    1
International Women's Day Promo     1
Victory Day CIS (May 9th) Event     1
CIS New Year Gift Lottery           1
Dragon Boat Festival Giveaway       1
Single's Day Gift Promo             1
Chinese Moon Festival               1
Name: name, dtype: int64

В значениях присутствуют данные о названиях кампаний, неявных дубликатов не наблюдается.

In [19]:
campaings['regions'].value_counts()
Out[19]:
APAC                        4
EU, CIS, APAC, N.America    3
EU, N.America               2
EU, CIS, APAC               2
CIS                         2
N.America                   1
Name: regions, dtype: int64

В значениях присутствуют данные о проведении кампаний в регионах и их сочетаниях. Неявных дубликатов не выявлено.

In [20]:
new_users['region'].value_counts()
Out[20]:
EU           46270
N.America     9155
CIS           3155
APAC          3153
Name: region, dtype: int64

Представлено 4 региона новых пользователей.

In [21]:
new_users['device'].value_counts()
Out[21]:
Android    27520
PC         15599
iPhone     12530
Mac         6084
Name: device, dtype: int64

Представлено 4 устройства, с которых происходила регистрация.

In [22]:
events['event_name'].value_counts()
Out[22]:
login           189552
product_page    125563
purchase         62740
product_cart     62462
Name: event_name, dtype: int64

В датафрейме с событиями пользователей представлено 4 вида событий: регистрация, просмотр корзины, конверсии в просмотр карточек товаров и покупка. Заменим именования событий для удобства восприятия.

In [23]:
def new_name(name):
    if name == 'login':
        return 'регистрация'
    elif name == 'product_page':
        return 'конверсии в просмотр карточек товаров'
    elif name == 'purchase':
        return 'покупка'
    elif name == 'product_cart':
        return 'просмотр корзины'
    else:
        return 'Unknown'

events['event_name'] = events['event_name'].apply(new_name)
events.head()
Out[23]:
user_id event_dt event_name details
0 E1BDDCE0DAFA2679 2020-12-07 20:22:03 покупка 99.99
1 7B6452F081F49504 2020-12-07 09:22:53 покупка 9.99
2 9CD9F34546DF254C 2020-12-07 12:59:29 покупка 4.99
3 96F27A054B191457 2020-12-07 04:02:40 покупка 4.99
4 1FD7660FDF94CA1F 2020-12-07 10:15:09 покупка 4.99
In [24]:
participants['group'].value_counts()
Out[24]:
A    9655
B    8613
Name: group, dtype: int64

В датафрейме с данными участников теста представлено разделение на 2 группы - А и В.

In [25]:
participants['ab_test'].value_counts()
Out[25]:
interface_eu_test          11567
recommender_system_test     6701
Name: ab_test, dtype: int64

Также мы наблюдаем 2 вида теста : так называемый наш тест по ТЗ о внедрении улучшенной рекомендательной системы. И тест связанный с изменениями с интерфейсом, проводимый с европейской аудиторией.

Проверим столбцы с датами.¶

In [26]:
print('В campaings представлены данные о начале и завершении маркетинговых событий за период:', campaings['finish_dt'].max()-campaings['start_dt'].min(),
      'с', campaings['start_dt'].min(), 'по',  campaings['finish_dt'].max())
В campaings представлены данные о начале и завершении маркетинговых событий за период: 348 days 00:00:00 с 2020-01-25 00:00:00 по 2021-01-07 00:00:00
In [27]:
print('В new_users представлены данные регистрации новых пользователей за период:', new_users['first_date'].max()-new_users['first_date'].min(),
      'с', new_users['first_date'].min(), 'по',  new_users['first_date'].max())
В new_users представлены данные регистрации новых пользователей за период: 16 days 00:00:00 с 2020-12-07 00:00:00 по 2020-12-23 00:00:00
In [28]:
print('В events представлены данные о событиях новых пользователей за период:', events['event_dt'].max()-events['event_dt'].min(),
      'с', events['event_dt'].min(), 'по',  events['event_dt'].max())
В events представлены данные о событиях новых пользователей за период: 23 days 23:36:00 с 2020-12-07 00:00:33 по 2020-12-30 23:36:33

Проверим группировку данных таблиц.¶

In [29]:
new_users['user_id'].nunique()
Out[29]:
61733

Столбец с идентификаторами пользователей содержит уникальные значения в каждой строке.

In [30]:
events['user_id'].nunique()
Out[30]:
58703

Мы делаем вывод, что не все пользователи совершали события, так как в таблице с событиями уникальных id меньше.

In [31]:
participants['user_id'].nunique()
Out[31]:
16666

Таблица participants содержит 18268 строк, уникальных id она содержит в меньшем количестве, соответственно, делаем вывод, что некоторые участники тестов попали в оба теста и повторились.

Вывод по предобработке данных.¶

  • Поменяли тип данных столбцов с временем на datetime

  • в столбце details заполнены значения только по стоимости покупки. Данных для остальных событий не предусмотрено. Соотвественно пропуски оставили.

Оценка корректности проведения теста.¶

Проверка соответствия данных требованиям технического задания.¶

Название теста по ТЗ recommender_system_test. Проверим соответствие данных ТЗ по всем участникам данного теста. Отберем участников нашего теста из таблицы participants.

In [32]:
rec_participants = participants.query('ab_test == "recommender_system_test"')
rec_participants
Out[32]:
user_id group ab_test
0 D1ABA3E2887B6A73 A recommender_system_test
1 A7A3664BD6242119 A recommender_system_test
2 DABC14FDDFADD29E A recommender_system_test
3 04988C5DF189632E A recommender_system_test
4 482F14783456D21B B recommender_system_test
... ... ... ...
6696 053FB26D6D49EDDC A recommender_system_test
6697 9D263B8EF15CF188 B recommender_system_test
6698 F2FBBA33F37DEC46 A recommender_system_test
6699 29C92313A98B1176 B recommender_system_test
6700 6715343AFBA285AE B recommender_system_test

6701 rows × 3 columns

В тесте recommender_system_test участвовал 6701 пользователь. Ожидаемое количество участников теста по ТЗ - 6000.

In [33]:
rec_participants.groupby(['ab_test', 'group']).agg({'user_id': 'nunique'})
Out[33]:
user_id
ab_test group
recommender_system_test A 3824
B 2877

Распределение по группам выглядит неравномерным. Разница почти в 1000 пользователей.

Проверим соответствие требованиям ТЗ по датам :

  • дата запуска: 2020-12-07;

  • дата остановки набора новых пользователей: 2020-12-21;

  • дата остановки: 2021-01-04;

Соединим таблицы по id пользователей и сверим даты с ТЗ.

In [34]:
new_rec_participants = rec_participants.merge(new_users, on='user_id')
new_rec_participants
Out[34]:
user_id group ab_test first_date region device
0 D1ABA3E2887B6A73 A recommender_system_test 2020-12-07 EU PC
1 A7A3664BD6242119 A recommender_system_test 2020-12-20 EU iPhone
2 DABC14FDDFADD29E A recommender_system_test 2020-12-08 EU Mac
3 04988C5DF189632E A recommender_system_test 2020-12-14 EU iPhone
4 482F14783456D21B B recommender_system_test 2020-12-14 EU PC
... ... ... ... ... ... ...
6696 053FB26D6D49EDDC A recommender_system_test 2020-12-10 N.America Android
6697 9D263B8EF15CF188 B recommender_system_test 2020-12-16 N.America Mac
6698 F2FBBA33F37DEC46 A recommender_system_test 2020-12-18 APAC Mac
6699 29C92313A98B1176 B recommender_system_test 2020-12-07 APAC Android
6700 6715343AFBA285AE B recommender_system_test 2020-12-07 CIS Android

6701 rows × 6 columns

In [35]:
new_rec_participants = new_rec_participants.merge(events, on='user_id', how='left')
new_rec_participants['user_id'].nunique()
Out[35]:
6701

Проверим дату регистрации.

In [36]:
print('Участники recommender_system_test регистрировались в период:', new_rec_participants['first_date'].max()-new_rec_participants['first_date'].min(),
      'с', new_rec_participants['first_date'].min(), 'по',  new_rec_participants['first_date'].max())
Участники recommender_system_test регистрировались в период: 14 days 00:00:00 с 2020-12-07 00:00:00 по 2020-12-21 00:00:00

Максимальная дата регистрации новых пользователей соответствует дате окончания набора новых пользователей. Минимальная дата, соответственно, дате начала запуска теста. В тесте участвовали только новые пользователи.

Дату окончания теста выбрали с тем смыслом, чтобы отследить также события пользователей, зарегистрировавшихся в последний день в течении последующих 14 дней, соответственно с 2020-12-21 по 2021-01-04.

Проверим даты событий пользователей.

In [37]:
print('Участники recommender_system_test совершали события в период:', new_rec_participants['event_dt'].max()-new_rec_participants['event_dt'].min(),
      'с', new_rec_participants['event_dt'].min(), 'по',  new_rec_participants['event_dt'].max())
Участники recommender_system_test совершали события в период: 23 days 12:37:00 с 2020-12-07 00:05:57 по 2020-12-30 12:42:57

У нас нет данных для 5 дней. Возможно были выгружены не все данные или на самом деле тест остановили раньше времени по ТЗ.

Согласно ТЗ в тест должно быть отобрано 15% новых пользователей из региона EU. Отфильтруем всех EU пользователей, которые зарегистрировались по 21.12.2020 и посчитаем долю участников нашего теста от них.

In [38]:
eu_users = new_users.query('first_date <= "2020-12-21 00:00:00"')[new_users.query('first_date <= "2020-12-21 00:00:00"')['region'] == 'EU']['user_id'].nunique()
test_eu_users = new_rec_participants[new_rec_participants['region'] == 'EU']['user_id'].nunique()
print('Доля участников теста из EU от общего количества участников из EU: {:.2%}'.format(test_eu_users / eu_users))
Доля участников теста из EU от общего количества участников из EU: 15.00%

Доля участников из EU, участвующих в тесте от общего числа зарегистрировавшихся в период набора участников, соответствует ТЗ.

Ожидаемый эффект по ТЗ: "За 14 дней с момента регистрации пользователи покажут улучшение каждой метрики не менее, чем на 10%".

Т.е. для исследовательского анализа мы оставим только те события, которые пользователи совершали в указанный период.

Время проведения теста. Пересечение с другими активностями.¶

Время проведения теста по ТЗ - 2020-12-07 по 2021-01-04.

Мы располагаем данными о совершенных событиях пользователями за период с 2020-12-07 по 2020-12-30.

Посмотрим какие кампании и когда проводились.

In [39]:
campaings.sort_values(by='start_dt')
Out[39]:
name regions start_dt finish_dt
6 Chinese New Year Promo APAC 2020-01-25 2020-02-07
1 St. Valentine's Day Giveaway EU, CIS, APAC, N.America 2020-02-14 2020-02-16
8 International Women's Day Promo EU, CIS, APAC 2020-03-08 2020-03-10
2 St. Patric's Day Promo EU, N.America 2020-03-17 2020-03-19
3 Easter Promo EU, CIS, APAC, N.America 2020-04-12 2020-04-19
7 Labor day (May 1st) Ads Campaign EU, CIS, APAC 2020-05-01 2020-05-03
9 Victory Day CIS (May 9th) Event CIS 2020-05-09 2020-05-11
11 Dragon Boat Festival Giveaway APAC 2020-06-25 2020-07-01
4 4th of July Promo N.America 2020-07-04 2020-07-11
13 Chinese Moon Festival APAC 2020-10-01 2020-10-07
12 Single's Day Gift Promo APAC 2020-11-11 2020-11-12
5 Black Friday Ads Campaign EU, CIS, APAC, N.America 2020-11-26 2020-12-01
0 Christmas&New Year Promo EU, N.America 2020-12-25 2021-01-03
10 CIS New Year Gift Lottery CIS 2020-12-30 2021-01-07

C 2020-12-25 по 2021-01-03 проводилась рождественско-новогодняя акция. Имеется пересечение с периодом проведения теста в 5 дней. Посмотрим как распределились события по дням.

In [40]:
new_rec_participants['event_date'] = new_rec_participants['event_dt'].dt.date # добавим столбец с датами
In [41]:
# посчитаем число событий для каждой даты
action_date = new_rec_participants.groupby('event_date').agg({'event_name':'count'}).reset_index()
action_date.head()
Out[41]:
event_date event_name
0 2020-12-07 709
1 2020-12-08 593
2 2020-12-09 746
3 2020-12-10 613
4 2020-12-11 542
In [42]:
sns.set(rc = {'figure.figsize':(20,8)})
sns.barplot(x = 'event_date',
            y = 'event_name',
            data = action_date,
            ci = 0,
           palette='seismic')

plt.title('Динамика событий по дням', fontsize=20)
plt.ylabel('Количество событий', fontsize=15)
plt.xlabel('Дата', fontsize=15)
plt.xticks(rotation=45)
plt.show()

В целом мы наблюдаем повышение активности в предпраздничные дни. На графике наблюдается два особенных пика событий 14 декабря и 21 декабря. С 21 декабря акивность пошла на спад. С 25 декабря особенной активности не наблюдается. Поэтому мы можем сделать вывод, что акция не повлияла на поведение пользователей. Значит с маркетинговыми активностями всплески не связаны, возможно есть другая причина. Посмотрим на динамику регистрации новых пользователей по тестовым группам.

In [43]:
# выведем для каждой даты регистрации каждого пользователя
users_date = new_rec_participants.groupby(['first_date', 'group']).agg({'user_id':'count'}).reset_index()
users_date['first_date'] = users_date['first_date'].dt.date
users_date.head()
Out[43]:
first_date group user_id
0 2020-12-07 A 1301
1 2020-12-07 B 1489
2 2020-12-08 A 795
3 2020-12-08 B 373
4 2020-12-09 A 636
In [44]:
sns.set(rc = {'figure.figsize':(20,8)})
sns.barplot(x = 'first_date',
            y = 'user_id',
            hue = 'group',
            data = users_date,
            estimator=sum,
            ci = 0,
           palette='seismic')

plt.title('Динамика регистраций участников по дням', fontsize=20)
plt.legend(title = 'Группа', fontsize=15)
plt.ylabel('Количество регистраций', fontsize=15)
plt.xlabel('Дата', fontsize=15)
plt.xticks(rotation=45)
plt.show()

График показывает, что именно 14 и 21 декабря шел активный набор группы А. Причину мы можем только предполагать.

Аудитория теста.¶

Проверка на пересечения с конкурирующим тестом.¶

In [45]:
# создадим срез с конкурирующим тестом
inter_participants = participants.query('ab_test == "interface_eu_test"')
inter_participants
Out[45]:
user_id group ab_test
6701 D4E530F6595A05A3 A interface_eu_test
6702 773ECB64E45DEBAB A interface_eu_test
6703 6BCB0F33D3BAB8C2 A interface_eu_test
6704 AABA4219186465C9 A interface_eu_test
6705 2BA8FA8754D1FE50 B interface_eu_test
... ... ... ...
18263 1D302F8688B91781 B interface_eu_test
18264 3DE51B726983B657 A interface_eu_test
18265 F501F79D332BE86C A interface_eu_test
18266 63FBE257B05F2245 A interface_eu_test
18267 79F9ABFB029CF724 B interface_eu_test

11567 rows × 3 columns

Посчитаем сколько пересечений между тестами.

In [46]:
print(len(np.intersect1d(new_rec_participants['user_id'].unique(), inter_participants['user_id'].unique())), 'пользователей принимали участие в обоих тестированиях.')
1602 пользователей принимали участие в обоих тестированиях.

В таком случае конкурирующий тест может повлиять на пользователей из нашего теста. Посмотрим доли таких пользователей в группах нашего теста.

In [47]:
round(len(np.intersect1d(new_rec_participants.query('group == "A"')['user_id'].unique(), inter_participants['user_id'].unique())) / new_rec_participants.query('group == "A"')['user_id'].nunique() * 100, 2)
Out[47]:
24.08
In [48]:
round(len(np.intersect1d(new_rec_participants.query('group == "B"')['user_id'].unique(), inter_participants['user_id'].unique())) / new_rec_participants.query('group == "B"')['user_id'].nunique() * 100, 2)
Out[48]:
23.67

В группе А нашего теста пересекающиеся участники занимают около 24 % от общего числа. В группе В - также приблизительно 24 %. Есть основание считать эти доли приблизительно равными. Соответственно, участники из двух тестов распределены равномерно по группам нашего теста, основания их удалять нет.

Проверка на равномерность распределения по тестовым группам.¶

In [49]:
len(np.intersect1d(new_rec_participants.query('group == "A"')['user_id'].unique(),
                   new_rec_participants.query('group == "B"')['user_id'].unique()))
Out[49]:
0

Пользователей, попавших в обе группы нашего теста не обнаружено.

Мы уже пришли к выводу, что не все пользователи совершали события, так как в таблице с событиями уникальных id меньше. Проверим, все ли пользователи, зарегистрированные в нашем тесте, совершали за период проведения теста события.

In [50]:
print('Доля активных участников всего теста: {:.2%}'.format(len(np.intersect1d(new_rec_participants['user_id'].unique(),
                                                                         events['user_id'].unique())) / new_rec_participants['user_id'].nunique()))
Доля активных участников всего теста: 54.84%

Посмотрим как активные пользователи распределились по группам.

In [51]:
print('Доля активных участников теста среди группы А: {:.2%}'.format(len(np.intersect1d(new_rec_participants.query('group == "A"')['user_id'].unique(), 
                                                                         events['user_id'].unique())) / new_rec_participants.query('group == "A"')['user_id'].nunique()))
Доля активных участников теста среди группы А: 71.84%
In [52]:
print('Доля активных участников теста среди группы B: {:.2%}'.format(len(np.intersect1d(new_rec_participants.query('group == "B"')['user_id'].unique(), 
                                                                         events['user_id'].unique())) / new_rec_participants.query('group == "B"')['user_id'].nunique()))
Доля активных участников теста среди группы B: 32.26%

Возможно, учитывая условие по ТЗ о 14 -дневном лайфтайме, мы отфильтруем невошедшие в него события, и, свместе с тем неактивных в этот период пользователей.

In [53]:
# отфильтруем события с лайфтаймом больше 14 дней
new_rec_participants = new_rec_participants[(new_rec_participants['event_dt'] - new_rec_participants['first_date']).dt.days <= 14]
print('Доля оставшихся после фильтрации пользователей: {:.2%}'.format(new_rec_participants['user_id'].nunique() / len(rec_participants)))
Доля оставшихся после фильтрации пользователей: 54.84%

Осталось около 55 % пользователей. Теперь проверим изменилась ли ситуация по неактивным пользователям.

In [54]:
print('Доля активных участников всего теста: {:.2%}'.format(len(np.intersect1d(new_rec_participants['user_id'].unique(),
                                                                         events['user_id'].unique())) / new_rec_participants['user_id'].nunique()))
Доля активных участников всего теста: 100.00%

Все неактивные участники теста отфильтровались с событиями, не входящими в 14 -дневный лайфтайм каждого участника.

Теперь посмотрим на итоговое распределение между группами нашего теста.

In [55]:
new_rec_participants.shape
Out[55]:
(24070, 10)
In [56]:
new_rec_participants['user_id'].nunique()
Out[56]:
3675
In [57]:
new_rec_participants.groupby(['ab_test', 'group']).agg({'user_id': 'nunique'})
Out[57]:
user_id
ab_test group
recommender_system_test A 2747
B 928

Пользователи также наравномерно распределены по группам теста. Перепроверим долю участников из EU.

In [58]:
eu_users = new_users.query('first_date <= "2020-12-21 00:00:00"')[new_users.query('first_date <= "2020-12-21 00:00:00"')['region'] == 'EU']['user_id'].nunique()
test_eu_users = new_rec_participants[new_rec_participants['region'] == 'EU']['user_id'].nunique()
print('Доля участников теста из EU от общего количества участников из EU: {:.2%}'.format(test_eu_users / eu_users))
Доля участников теста из EU от общего количества участников из EU: 8.22%

Вывод по корректности проведения теста.¶

В целом тест проведен с нарушенияем ряда условий, что может привести к некорректному результату:

  • Разделение участников по группам теста неравномерно

  • У нас нет данных для 5 последних дней до даты окончания теста по ТЗ. Возможно были выгружены не все данные или на самом деле тест остановили раньше времени по ТЗ

  • в период проведения теста проводилась акция, она не повлияла особенным образом на пользовательскую активность, но рекомендуем не проводить акции паралллельно с тестированием

  • Динамика событий по дням неравномерна, так как период проведения теста пришелся на рожденственские праздники, что повлияло на поведение участников теста. Также наблюдается повышенная активность пользователей группы А - 14 и 21 декабря, что рекомендуем изучить подробнее, проконсультировавшись с отделом маркетинга

  • мы выявили пересечение участников с конкурирующим тестом, что оказывает влияние на участников нашего теста. Распределение долей по группам нашего теста показало примерно одинаковую долю общих с конкурирующим тестом участников, мы делаем вывод, что повлиял он на них в одинаковой степени. Рекомендуем отслеживать состав групп в будущих тестированиях.

  • в тесте участвовало всего около 55 % активных пользователей. После фильтрации дат событий, входящих в 14-дневный лайфтайм каждого пользователя, неактивные участники отсеялись, но вместе с тем снизилась доля участников из EU от общего числа новых пользователей до 8,22 %. При нефильтрованных данных доля соответствовала ТЗ.

Исследовательский анализ данных.¶

Количество событий на пользователя одинаково распределены в выборках?¶

Посчитаем среднее количество событий на участника.

In [59]:
print('Среднее количество событий на пользователя: ', round(len(new_rec_participants['event_name']) / new_rec_participants['user_id'].nunique()))
Среднее количество событий на пользователя:  7
In [60]:
# посчитаем число событий для каждого пользователя
users_events = new_rec_participants.groupby(['user_id', 'group']).agg({'event_name':'count'}).reset_index()
users_events.head()
Out[60]:
user_id group event_name
0 001064FEAAB631A1 B 6
1 0010A1C096941592 A 12
2 00341D8401F0F665 A 2
3 003DF44D7589BBD4 A 15
4 00505E15A9D81546 A 5
In [61]:
sns.set(rc = {'figure.figsize':(20,8)})
sns.boxplot(data = users_events, y='event_name', x=users_events['group'].sort_values(), palette='seismic')
plt.title('Распределение количества событий на пользователя в группах.', fontsize=20)
plt.xlabel('Группы', fontsize=15)
plt.ylabel('Количество событий на пользователя', fontsize=15)
plt.suptitle('')
plt.show()
In [62]:
users_events[users_events['group'] == 'A']['event_name'].describe()
Out[62]:
count    2747.000000
mean        6.897343
std         3.840267
min         1.000000
25%         4.000000
50%         6.000000
75%         9.000000
max        24.000000
Name: event_name, dtype: float64
In [63]:
users_events[users_events['group'] == 'B']['event_name'].describe()
Out[63]:
count    928.000000
mean       5.520474
std        3.303036
min        1.000000
25%        3.000000
50%        4.000000
75%        8.000000
max       24.000000
Name: event_name, dtype: float64

Среднее количество событий на пользователя в группе А - около 7(медиана - 6), в группе В - около 5(медиана - 4). Минимальные и мксимальные значения в выборках совпадают. Стандартное отклонение примерно одинаково. Можно сделать вывод, что события в обеих выборках распределены почти одинаково.

Как число событий в выборках распределено по дням?¶

In [64]:
#создадим группировку по датам и посчитаем среднее количество событий на пользователя по дням
date_events = new_rec_participants.groupby(['event_date', 'group']).agg({'event_name':'count', 'user_id':'nunique'}).reset_index()
date_events['users_mean_events'] = round(date_events['event_name'] / date_events['user_id'], 2)
date_events.head()
Out[64]:
event_date group event_name user_id users_mean_events
0 2020-12-07 A 331 154 2.15
1 2020-12-07 B 378 173 2.18
2 2020-12-08 A 341 160 2.13
3 2020-12-08 B 252 120 2.10
4 2020-12-09 A 385 178 2.16
In [65]:
sns.set(rc = {'figure.figsize':(20,8)})
sns.barplot(x = 'event_date',
            y = 'users_mean_events',
            hue = 'group',
            data = date_events,
            ci = 0,
            palette='seismic')

plt.title('Динамика среднего количества событий участника по дням', fontsize=20)
plt.legend(title = 'Группа', fontsize=15)
plt.ylabel('Количество событий', fontsize=15)
plt.xlabel('Дата', fontsize=15)
plt.xticks(rotation=45)
plt.show()

Мы набюдаем рост количества событий на пользователя в группе А с 14 и с 24 декабря. Выше мы выявили, что 14 декабря шел активный набор в группу А. Причину того, что могло повлиять на динамику с 24 декабря мы не можем определить , рекомендуем проконсультироваться с отделом маркетинга.

Как меняется конверсия в воронке в выборках на разных этапах?¶

Посмотрим, какие события есть в логах, как часто они встречаются.

In [66]:
event_grouped = (
    new_rec_participants.pivot_table(index = 'event_name', columns='group', values='user_id', aggfunc='nunique')
    .reset_index() 
    .reindex([3,0,2,1])
)
event_grouped['total_events'] = event_grouped['A'] + event_grouped['B']
event_grouped['share%'] = round(event_grouped['total_events'] / sum(event_grouped['total_events']) * 100, 2)

event_grouped
Out[66]:
group event_name A B total_events share%
3 регистрация 2747 927 3674 44.89
0 конверсии в просмотр карточек товаров 1780 523 2303 28.14
2 просмотр корзины 824 255 1079 13.18
1 покупка 872 256 1128 13.78

На регистрацию приходится 45 % всех событий. Около 28 % событий -это конверсии в просмотр карточек товаров. Примерно одинаковые доли приходятся на просмотр корзины и покупку - около 13 %.

In [67]:
event_grouped_a = (
    new_rec_participants.query('group == "A"')
    .groupby('event_name')
    .agg({'event_name':'count', 'user_id':'nunique'})    
)
event_grouped_a.columns = ['events', 'users_unique']
event_grouped_a['share%'] = round((event_grouped_a['users_unique'] / new_rec_participants['user_id'].nunique() * 100), 2)
event_grouped_a = event_grouped_a.reset_index().reindex([3,0,2,1])
event_grouped_a
Out[67]:
event_name events users_unique share%
3 регистрация 8400 2747 74.75
0 конверсии в просмотр карточек товаров 5415 1780 48.44
2 просмотр корзины 2519 824 22.42
1 покупка 2613 872 23.73
In [68]:
event_grouped_b = (
    new_rec_participants.query('group == "B"')
    .groupby('event_name')
    .agg({'event_name':'count', 'user_id':'nunique'})
    .sort_values(by='user_id', ascending=False)
)
event_grouped_b.columns = ['events', 'users_unique']
event_grouped_b['share%'] = round((event_grouped_b['users_unique'] / new_rec_participants['user_id'].nunique() * 100), 2)
event_grouped_b = event_grouped_b.reset_index().reindex([0,1,3,2])
event_grouped_b
Out[68]:
event_name events users_unique share%
0 регистрация 2493 927 25.22
1 конверсии в просмотр карточек товаров 1331 523 14.23
3 просмотр корзины 659 255 6.94
2 покупка 640 256 6.97

Построим диаграмму воронки событий.

In [69]:
fig = go.Figure()

fig.add_trace(go.Funnel(
    name = 'Группа А',
    y = event_grouped_a['event_name'],
    x = event_grouped_a['users_unique'],
    textinfo = "value+percent initial"))

fig.add_trace(go.Funnel(
    name = 'Группа В',
    orientation = "h",
    y = event_grouped_b['event_name'],
    x = event_grouped_b['users_unique'],
    textposition = "inside",
    textinfo = "value+percent previous"))

fig.update_layout(title='Воронка пользователей по событиям')

Можем предположить, что примерный путь пользователей группы А и группы В выглядит так:

  1. Регистрация

  2. Переход в просмотр карточек товаров

  3. Просмотр корзины

  4. Покупка

Можно предположить, что есть возможность покупки, минуя корзину. Поэтому конверсий в корзину меньше.

Также можем отметить, что около 32 % пользователей в группе А и около 28 % в группе В, зарегистрировавшихся пользователей доходит до страницы с успешной оплатой.

Посчитаем , какая доля пользователей проходит на следующий шаг воронки от числа пользователей на предыдущем.

In [70]:
users_share_a = event_grouped_a.loc[:, ('event_name', 'users_unique')]
users_share_a['share_after_step'] = round((users_share_a['users_unique'] / users_share_a['users_unique'].shift())[1:], 2)
users_share_a
Out[70]:
event_name users_unique share_after_step
3 регистрация 2747 NaN
0 конверсии в просмотр карточек товаров 1780 0.65
2 просмотр корзины 824 0.46
1 покупка 872 1.06
In [71]:
users_share_b = event_grouped_b.loc[:, ('event_name', 'users_unique')]
users_share_b['share_after_step'] = round((users_share_b['users_unique'] / users_share_b['users_unique'].shift())[1:], 2)
users_share_b
Out[71]:
event_name users_unique share_after_step
0 регистрация 927 NaN
1 конверсии в просмотр карточек товаров 523 0.56
3 просмотр корзины 255 0.49
2 покупка 256 1.00

Группа А:

  • Примерно 65% зарегистрировавшихся пользователей посетили страницу с каталогом.
  • Посетив страницу с каталогом, около 46 % пользователей перешли на страницу с корзиной.
  • Так как возможно покупки совершались, минуя этап корзины, то конечные этапы просмотр корзины и покупка перемешались.
  • Можно утвеждать, что на шаге страница карточек товаров - просмотр корзины теряется больше всего пользователей-около 54 %.

Группа В:

  • Примерно 56% зарегистрировавшихся пользователей посетили страницу с каталогом.
  • Посетив страницу с каталогом, около 49 % пользователей перешли на страницу с корзиной.
  • Так как возможно покупки совершались, минуя этап корзины, то конечные этапы просмотр корзины и покупка перемешались.
  • Можно утвеждать, что на шаге страница карточек товаров - просмотр корзины теряется больше всего пользователей-около 51 %.

    В целом можно сделать вывод, что воронки событий обеих групп одинаковы по порядку этапов, в обеих группах наблюдаются большие потери пользователей(более 50 %) на шаге страница карточек товаров - просмотр корзины. Рекомендуем обратить внимание на то, почему достаточно большое количество пользователей не переходят на страницу с корзиной.

    Конверсия в этап страница с каталогом ниже в тестовой группе на 9 %.

Вывод по исследовательскому анализу.¶

  • события в обеих выборках распределены почти одинаково

  • наблюдаются скачки событий 14 и 21 декабря. Возможно это связано с активным набором в группу А. Также заметно влияние рождественских праздников на днамику

  • мы выяснили, что переходов в корзину меньше, чем в покупки и в общем и при делении на группы теста. Возможно переход в покупки осуществлялся, минуя корзину. В связи с этим мы решили считать порядок этапов в логическом порядке:

  1. Регистрация

  2. Переход в просмотр карточек товаров

  3. Просмотр корзины

  4. Покупка

  • в обеих группах наблюдаются большие потери пользователей(более 50 %) на шаге страница карточек товаров - просмотр корзины(оплата). Рекомендуем обратить внимание на то, почему достаточно большое количество пользователей не переходят на страницу с корзиной или оплатой.

Оценка результатов А/В тестирования.¶

Проверка гипотезы о равенстве долей.¶

Нам необходимо проверить, находят ли статистические критерии разницу между долям в группах А и В при конверсии в этапы событий.

Нулевая гипотеза: различий между долями пользователей, совершивших событие, нет

Альтернативная гипотеза: различия между долями пользователей, совершивших событие, есть.

Чтобы снизить вероятность ложнопозитивного результата при множественном тестировании гипотез, мы применим поправку Бонферрони. Данные двух мы сравним по трем событиям (конверсии в просмотре карточек товаров, в просмотр корзины, в покупки т.к. на шаге регистрация 100% пользователей и сокращать из-за этого alpha не нужно).

Следовательно, bonferroni_alpha будет равно alpha / 3. В нашем случае мы будем использовать proportions_ztest для двух выборок. Мы сравниваем пропорции из этих выборках на разных этапах.

In [72]:
event_grouped_ab = new_rec_participants.pivot_table(index = 'event_name', 
                                                    columns = 'group', values = 'user_id', aggfunc = 'nunique')
event_grouped_ab
Out[72]:
group A B
event_name
конверсии в просмотр карточек товаров 1780 523
покупка 872 256
просмотр корзины 824 255
регистрация 2747 927
In [73]:
alpha = 0.05
print('bonferroni_alpha =', round(alpha/3, 6))
bonferroni_alpha = 0.016667
In [74]:
def p_z_test(successes1, successes2, trials1, trials2, bonferroni_alpha = 0.016667):

    counts = [successes1, successes2]
    nobs = [trials1, trials2]
    
    print(counts, nobs)
    
    stat, p_value = proportions_ztest(count = counts, nobs = nobs, alternative = 'two-sided')

    print('z_stat: %0.3f, p_value: %0.6f' % (stat, p_value))

    if p_value < bonferroni_alpha:
        print('Отвергаем нулевую гипотезу: между долями есть разница')
    else:
        print(
        'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
In [75]:
users_a = new_rec_participants.query('group == "A"')['user_id'].nunique() #количество уникальных пользователей гр.А 
In [76]:
users_b = new_rec_participants.query('group == "B"')['user_id'].nunique() #количество уникальных пользователей гр.В
In [77]:
for event in new_rec_participants.query('event_name != "регистрация"')['event_name'].unique():
    print('Событие:', event)
    p_z_test(event_grouped_ab.loc[event, 'A'],
            event_grouped_ab.loc[event, 'B'],
            users_a,
            users_b)
    print()
Событие: покупка
[872, 256] [2747, 928]
z_stat: 2.374, p_value: 0.017592
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Событие: просмотр корзины
[824, 255] [2747, 928]
z_stat: 1.456, p_value: 0.145348
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Событие: конверсии в просмотр карточек товаров
[1780, 523] [2747, 928]
z_stat: 4.596, p_value: 0.000004
Отвергаем нулевую гипотезу: между долями есть разница

Можно сделать вывод, что различий между долями уникальных пользователей среди групп А и В при конверсии в просмотр корзины и в покупку нет.

Общий вывод.¶

Мы оценили корректность проведения теста и проанализировали результаты теста.

Мы считаем, что тест был проведен некорректно:

  1. У нас нет данных для 5 последних дней до даты окончания теста по ТЗ. Возможно были выгружены не все данные или на самом деле тест остановили раньше времени по ТЗ

  2. в период проведения теста проводилась акция, она не повлияла особенным образом на пользовательскую активность, но рекомендуем не проводить акции паралллельно с тестированием

  3. Динамика событий по дням неравномерна, так как период проведения теста пришелся на рожденственские праздники, что повлияло на поведение участников теста. Также наблюдается повышенная активность пользователей группы А - 14 и 21 декабря, что совпадает с активным набором пользователей в группу А, рекомендуем изучить этот вопрос подробнее, проконсультировавшись с отделом маркетинга.

  4. Параллельно с нашим тестом проводился другой тест. 1602 пользователей принимали участие в обоих тестированиях, что по нашим расчетам возможно не повлияло на результаты, так как доли подобных пользователей распределились равномерно между группами и одинаково повлияли на каждую из них. Настоятельно рекомендуем не проводить параллельно несколько тестов, это может привести к искажению результатов.

  5. В тесте recommender_system_test участвовал 6701 пользователь. Ожидаемое количество участников теста по ТЗ - 6000. После удаления участников, не совершавших события, мы получили всего 3675 пользователей.

  6. Распределение по группам теста выглядит неравномерным. Разница почти в 1000 пользователей по начальным данным и 1819 после фильтрации неактивных пользователей. Рекомендуем отслеживать наполняемость групп.

  7. Доля участников из EU, участвующих в тесте от общего числа зарегистрировавшихся в период набора участников, по сырым данным соответствует ТЗ (15%). После фильтрации неактивных пользватлей доля участников из EU снизилась до 8,22 %.

  8. Анализ воронки событий выявил, что:

  • не отслеживается логичный путь регистрация-просмотр карточки товара-корзина-оплата. Конверсий на этап просмотр корзины меньше, чем в оплату. Возможная причина-переход к оплате , минуя этап корзина. Этот момент повлиял на расчеты конверсий.

  • в обеих группах наблюдаются большие потери пользователей(более 50 %) на шаге страница карточек товаров - просмотр корзины. Рекомендуем обратить внимание на то, почему достаточно большое количество пользователей не переходят на страницу с корзиной.

  • Также можем отметить, что около 32 % пользователей в группе А и около 28 % в группе В, зарегистрировавшихся пользователей доходит до страницы с успешной оплатой.

  • Конверсия в этап страница с каталогом ниже в тестовой группе на 9 %.

Проверка гипотезы о равенстве долей показала, что что различий между долями уникальных пользователей среди групп А и В при конверсии в просмотр корзины и в покупку нет. Это значит, что нововведение не повлияло каким -либо образом на конверсию на этих этапах.

Обнаружена разница между долями на этапе просмотр карточек товаров. Доля потерянных пользователей с этапа регистрация у группы А около 35 %, у группы В-около 44 %. Соответственно можно сделать вывод, что группа В проигрывает-нововведение не сработало лучшим образом. Следовательно, внедрение улучшенной рекомендательной системы не влияет положительным образом на метрики, а в некоторых случаях, имеет более низкие показатели в сравнении с контрольной группой.

Некорректное проведение теста, могло исказить результаты нашего исследования. Рекомендуем перезапустить тест, учитывая моменты, освещенные в выводе.